Praca domowa 3

Urbala Anna

In [1]:
# załadowanie modelu
import pickle
model = pickle.load(open("../../../../WB-XAI-Projekt/RF_model", "rb"))
In [2]:
import pandas as pd
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
from sklearn.impute import SimpleImputer
from sklearn.model_selection import train_test_split


# Wczytanie i przygotowanie danych 
full_data = pd.read_csv("hotel_bookings.csv")
full_data["agent"] = full_data["agent"].astype(str)
treshold = 0.005 * len(full_data)
agents_to_change = full_data['agent'].value_counts()[full_data['agent'].value_counts() < treshold].index
full_data.loc[full_data["agent"].isin(agents_to_change), "agent"] = "other"

countries_to_change = full_data['country'].value_counts()[full_data['country'].value_counts() < treshold].index
full_data.loc[full_data["country"].isin(countries_to_change), "country"] = "other"


# Określenie cech uwzględnionych w modelu
num_features = ["lead_time", "arrival_date_week_number",
                "stays_in_weekend_nights", "stays_in_week_nights", 
                "adults", "previous_cancellations",
                "previous_bookings_not_canceled",
                "required_car_parking_spaces", "total_of_special_requests", 
                "adr", "booking_changes"]

cat_features = ["hotel", "market_segment", "country", 
                "reserved_room_type",
                "customer_type", "agent"]

features = num_features + cat_features

# Podział na zmienne wyjaśniające i target
X = full_data.drop(["is_canceled"], axis=1)[features]
y = full_data["is_canceled"]

categorical_names = {}
for feature in cat_features:
    col = X[[feature]]
    cat_transformer = SimpleImputer(strategy="constant", fill_value="Unknown")
    col = cat_transformer.fit_transform(col)
    X[feature] = col
    le = LabelEncoder()
    le.fit(X[[feature]])
    X[[feature]] = le.transform(X[[feature]])
    categorical_names[feature] = le.classes_

categorical_names
# Preprocessing
num_transformer = SimpleImputer(strategy="constant")

preprocessor = ColumnTransformer(transformers=[("num", num_transformer, num_features)],
                                remainder = 'passthrough')

for feature in num_features:
    X[feature] = X[feature].astype(float)

X_train, X_test, y_train, y_test = train_test_split(
    X, y,
    test_size=0.2, random_state=42)
/home/anna/.local/lib/python3.6/site-packages/sklearn/utils/validation.py:63: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  return f(*args, **kwargs)
/home/anna/.local/lib/python3.6/site-packages/sklearn/utils/validation.py:63: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  return f(*args, **kwargs)
/home/anna/.local/lib/python3.6/site-packages/sklearn/utils/validation.py:63: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  return f(*args, **kwargs)
/home/anna/.local/lib/python3.6/site-packages/sklearn/utils/validation.py:63: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  return f(*args, **kwargs)
/home/anna/.local/lib/python3.6/site-packages/sklearn/utils/validation.py:63: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  return f(*args, **kwargs)
/home/anna/.local/lib/python3.6/site-packages/sklearn/utils/validation.py:63: DataConversionWarning: A column-vector y was passed when a 1d array was expected. Please change the shape of y to (n_samples, ), for example using ravel().
  return f(*args, **kwargs)

Dla wybranej obserwacji ze zbioru danych wylicz predykcję modelu.

In [3]:
selected_X = X_train.iloc[[420]]
selected_y = y_train.iloc[420]
predicted_y = model.predict(selected_X)
print("Prawdziwa wartość:", selected_y)
print("Przewidziana wartość:", predicted_y)
selected_X.head()
Prawdziwa wartość: 0
Przewidziana wartość: [0]
Out[3]:
lead_time arrival_date_week_number stays_in_weekend_nights stays_in_week_nights adults previous_cancellations previous_bookings_not_canceled required_car_parking_spaces total_of_special_requests adr booking_changes hotel market_segment country reserved_room_type customer_type agent
87246 29.0 16.0 0.0 3.0 1.0 0.0 0.0 0.0 1.0 83.57 0.0 0 6 21 0 2 17

Nasz model znowu poprawnie przewidział nieodwołanie rezerwacji.

Dla wybranej obserwacji z punktu 1., wylicz dekompozycję predykcji modelu używając profili Ceteris Paribus/ ICE (pakiety w R: DALEX, ALEPlot, ingredients, pakiety w Python: dalex, pyCeterisParibus)

In [4]:
import dalex as dx
explainer = dx.Explainer(model, X_train, y_train, label = "Random Forest")
Preparation of a new explainer is initiated

  -> data              : 95512 rows 17 cols
  -> target variable   : Parameter 'y' was a pandas.Series. Converted to a numpy.ndarray.
  -> target variable   : 95512 values
  -> model_class       : sklearn.ensemble._forest.RandomForestClassifier (default)
  -> label             : Random Forest
  -> predict function  : <function yhat_proba_default at 0x7f3cffd46598> will be used (default)
  -> predict function  : Accepts only pandas.DataFrame, numpy.ndarray causes problems.
  -> predicted values  : min = 0.0, mean = 0.371, max = 1.0
  -> model type        : classification will be used (default)
  -> residual function : difference between y and yhat (default)
  -> residuals         : min = -0.943, mean = -0.00218, max = 0.957
  -> model_info        : package sklearn

A new explainer has been created!
In [5]:
cp = explainer.predict_profile(selected_X)
cp.plot()
Calculating ceteris paribus: 100%|██████████| 17/17 [00:02<00:00,  7.94it/s]
In [9]:
print(categorical_names["country"][16])
print(categorical_names["country"][17])
PRT
RUS

Widzimy, że dla wybranej obserwacji prawdopodobieństwo rezygnacji jest niemalże zerowe (co jest zgodne z poprzednim zadaniem domowym, tam prawdopodobieństwo rezygnacji dla tej obserwacji wynosiło 1%). Jest tylko jedna zmienna, która mogłaby zmienić predykcję modelu dla tej obserwacji, gdyby sama została zmieniona - jest to previous_cancellations (co jest w sumie dość logicznym wnioskiem). Dość wysokie podbicie prawdopodobieństwa daje jeszcze zmiana kraju (swoją drogą tu znowu jest problem z kodowaniem zmiennych kategorycznych, przy one hot encodingu działa, przy label encodingu już nie). Mimo interpretacji ciągłej widać, że krajem, dla którego wzrasta prawdopodobieństwo rezygnacji jest Portugalia. A krajem, w którym znajdują się analizowane hotele też jest Portugalia. Wniosek? Turyści z zagranicy rzadziej rezygnują. Przy okazji warto zauważyć, że dość duże podbicie rezygnacji jest dla kraju nr 17. Można się zastanawiać, czy to kwestia tego, że ma numer bliski 16 i wpływ się przeniósł trochę dalej, czy to po prostu kwestia tego, że to Rosja.

Przy innych zmiennych raczej nie dzieje się nic ciekawego. Choć warto wspomnieć o zmiennej lead_time, której wzrost do pewnego momentu (mniej więcej do roku) daje niemal liniowy wzrost prawdopodobieństwa rezygnacji. No i brak specjalnych żądań zwiększa prawdopodobieństwo rezygnacji (co już też zostało potwierdzone przez poprzednie prace domowe).

Wybierz dwie obserwacje ze zbioru danych, które mają różne profile CP (na przykład: dla obserwacji A odpowiedź modelu rośnie wraz ze wzrostem wartości zmiennej, a dla obserwacji B maleje). Zauważ, żeby mieć takie różnice, musisz mieć model z interakcjami.

In [19]:
X1 = X_train.iloc[[420, 2137], :] #total of special requests
X2 = X_train.iloc[[420, 13], :] #stays in weekend nights
X3 = X_train.iloc[[420, 72], :] #stays in weekend nights
cp = explainer.predict_profile(X3)
cp.plot()
Calculating ceteris paribus: 100%|██████████| 17/17 [00:02<00:00,  7.99it/s]

Przez to, że na naszych wykresach niewiele się dzieje (a tam gdzie coś się dzieje, to zazwyczaj bardzo podobnie), trudno było znaleźć odwrotny wpływ jakiejś zmiennej. Udało mi się znaleźć 2 dość dobre przykłady - oba w porównaniu do obserwacji z poprzedniego podpunktu.

Dla obserwacji X3 stays_in_weekend_nights wraz ze wzrostem wpływa negatywnie na prawdopodobieństwo rezygnacji, mimo że dla poprzedniej obserwacji wpływało pozytywnie.

In [20]:
cp = explainer.predict_profile(X1)
cp.plot()
Calculating ceteris paribus: 100%|██████████| 17/17 [00:02<00:00,  8.08it/s]

Ten przypadek jest moim zdaniem ciekawszy, bo "schodek" przy zmiennej total_of_special_requests jest w drugą stronę niż w obu poprzednich przykładach. Czyli czasem brak specjalnych żądań wpływa pozytywnie na prawdopodobieństwo braku rezygnacji.

Wnioski:

  • Zastosowany encoding powinien zależeć od metody, której będziemy używać. Zostawiłam tu już label encoding, bo nie miał wpływu na zadanie domowe, ale warto o tym pamiętać na przyszłość.
  • Przy dużej liczbie zmiennych w modelu ciężko się przegląda profile Ceteris Paribus
  • Nasze predykcje są dość stabilne, wpływ danej zmiennej na predykcję zbliżony w większości przypadków.
In [ ]: